package com.xaicode.learn.netty.groupchat;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * @author Locker xaicode@sina.com
 * @version 1.0
 * @date 2021/6/17 0017 22:09
 */
@Slf4j
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

    private static final String CLIENT_TEXT = " [client %s] ";
    private static final String SELF_TEXT = " [self] ";

    String timedPrefix(String text) {
        // yyyy-MM-dd HH:mm:ss
        return DateUtil.format(new Date(), DatePattern.NORM_DATETIME_PATTERN) + text;
    }

    /*
        ChannelGroup: A thread-safe Set that contains open Channels and provides various bulk operations on them.

        GlobalEventExecutor: Single-thread singleton EventExecutor.
     */
    // Creates a new group with a generated name and the provided EventExecutor to notify the ChannelGroupFutures.
    private static final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    // Gets called after the ChannelHandler was added to the actual context and it's ready to handle events.
    // the first to run on connecting ok
    // cache current channel to channelGroup
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();

        /*
            channelGroup.writeAndFlush() core code on below:

            for (Channel c: nonServerChannels.values()) {
                if (matcher.matches(c)) {
                    futures.put(c, c.writeAndFlush(safeDuplicate(message)));
                }
            }
         */

        // notify all other client a new client is online
        channelGroup.writeAndFlush(String.format(timedPrefix(CLIENT_TEXT) + "add group", channel.remoteAddress()));
        // add current channel to channelGroup
        channelGroup.add(channel);
    }

    // Calls ChannelHandlerContext.fireChannelActive() to forward to the next ChannelInboundHandler in the ChannelPipeline.
    // notify others a new client is online
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        log.info(String.format(timedPrefix(CLIENT_TEXT) + "is online", ctx.channel().remoteAddress()));
    }

    // Gets called after the ChannelHandler was removed from the actual context and it doesn't handle events anymore.
    // auto remove current channel from channelGroup
    // current client leave forever and notify others the leaving thing
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        channelGroup.writeAndFlush(String.format(timedPrefix(CLIENT_TEXT) + "is leave", ctx.channel().remoteAddress()));
        log.info("current online number is {}", channelGroup.size());
    }

    // Calls ChannelHandlerContext.fireChannelInactive() to forward to the next ChannelInboundHandler in the ChannelPipeline.
    // notify others a client is offline
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        log.info(String.format(timedPrefix(CLIENT_TEXT) + "is offline", ctx.channel().remoteAddress()));
    }

    // Is called for each message.
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Channel channel = ctx.channel();

        // each check
        channelGroup.forEach(ch -> {
            if (channel.equals(ch)) {
                // self echo
                ch.writeAndFlush(String.format(timedPrefix(SELF_TEXT) + "msg: %s", msg));
            } else {
                // others
                ch.writeAndFlush(String.format(timedPrefix(CLIENT_TEXT) + "send msg: %s", ch.remoteAddress(), msg));
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // close channel
        ctx.close();
    }
}
